package com.apchernykh.rozhlasapp.player.fallback;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.net.URL;

import com.apchernykh.rozhlasapp.player.fallback.BaseFallbackPlayer.EPlayerState;
import com.apchernykh.rozhlasapp.ui.RozhlasAppBaseFragmentActivity;
import com.apchernykh.rozhlasapp.utils.ConditionalLog;

import android.content.Context;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

public abstract class RozhlasFallbackPlayer extends BaseFallbackPlayer {

	private static final String mFallbackPlaybackFileName = "krevetko.mp3";
	private static final int mPrebufferSeconds = 16;
	private static final int mDiskBufferSeconds = 300;
	final String mStreamUri;
	final int mRawSize;
	int mBytesPerSecond;
	static Thread mWriterThread = null;
	boolean mThreadAlive = true;
	InputStream mInputStream = null;
	String mBufferName = mFallbackPlaybackFileName;
	String mPrivateFilePath = "";
	int mCurrentRafPosition = 0;
	final int mBufSize;
	byte[] mRestOfFile;
	byte[] mOnDiskBuffer;
	URL mTargetUrl = null;
	File mPrivateFile = null;
	FileOutputStream mFileOut = null;
	RandomAccessFile mBufferFile = null;
	EPlayerState mMyState = EPlayerState.STOPPED;
	static RozhlasFallbackPlayer mFallbackPlayerToMonitor = null;

	@Override
	public boolean isValid() {
		return true;
	}

	@Override
	public void stop() {

		ConditionalLog.e("RozhlasFallbackPlayer::stop() called", new Throwable(
				"show me callstack"));
		mMyState = EPlayerState.STOPPED;
		dispose();

		// TODO: resolve race here
		if (mWriterThread == null)
			return;
		mThreadAlive = false;
		if (mWriterThread.isAlive()) {
			if (mInputStream != null) {
				try {
					mInputStream.close();
					mInputStream = null;
					mWriterThread.join(500);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
					// RozhlasAppBaseFragmentActivity.mApplicationModel.mInetInfo.signalInetFailure();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}

		mWriterThread = null;

	}

	@Override
	public void onCompletion(MediaPlayer iPlayer) {
		if (ConditionalLog.isEnabled()) {
			ConditionalLog.d(toString(), " playbackCompletedListener ");
		}
		if ((mMyState == EPlayerState.PLAYING || mMyState == EPlayerState.UNDERRUN)
				&& mThreadAlive && mSeparateThreadMediaPlayer != null) {
			startIfAdvancedEnough();
		} else {
			// if we dispose media player - just start writing to the disk
			// buffer from the beginning
			mCurrentRafPosition = 0;
			mMyState = EPlayerState.STOPPED;
			super.onCompletion(iPlayer);
		}
	}

	@Override
	public boolean onError(MediaPlayer iPlayer, int iWhat, int iExtra) {
		mMyState = EPlayerState.ERROR;
		mCurrentRafPosition = 0;
		return super.onError(iPlayer, iWhat, iExtra);
	}

	@Override
	public void start() {
		mFallbackPlayerToMonitor = this;
		dispose(); // TODO: remove this.
		mMyState = EPlayerState.UNDERRUN;
		mWriterThread = new Thread(mWriterSmart);
		mWriterThread.setName("MplayerBugWorkaround");
		mWriterThread.start();
	}

	public RozhlasFallbackPlayer(Context iContext, String iStreamUri) {
		super(iContext);
		mStreamUri = iStreamUri;

		mBytesPerSecond = 16000;
		if (iStreamUri.contains("32")) {
			mBytesPerSecond = 4000;
		}
		mRawSize = mBytesPerSecond * mDiskBufferSeconds;
		mBufSize = mBytesPerSecond * mPrebufferSeconds / 2 + 1;

		mRestOfFile = new byte[mBufSize];
		mOnDiskBuffer = new byte[mBufSize];
	}

	
	abstract boolean tryRecognizeFrame(byte[] iBuffer, int iOffset);

	int findFrameStart(byte[] iBuffer, int iLength) {
		int aFrameStart = -1;
		for (int aOffset = 0; aOffset < (iLength - 8); ++aOffset) {
			if (tryRecognizeFrame(iBuffer, aOffset)) {
				aFrameStart = aOffset;
				return aFrameStart;
			}
		}
		return aFrameStart;
	}

	int findFrameStartReverse(byte[] iBuffer, int iLength) {
		int aFrameStart = -1;
		for (int iOffset = (iLength - 8); iOffset > 0; --iOffset) {
			if (tryRecognizeFrame(iBuffer, iOffset)) {
				aFrameStart = iOffset;
				return aFrameStart;
			}
		}
		return aFrameStart;
	}

	// These two methods guarantee some hysteresis, to avoid excessive
	// starts/stops
	private boolean isPreBufferEnoughBytes() {
		return (calculateBufferAdvantageBytes() > (mPrebufferSeconds * mBytesPerSecond));
	}

	private boolean isBufferUnderrunBytes() {
		return (calculateBufferAdvantageBytes() < (mPrebufferSeconds * mBytesPerSecond) / 4);
	}

	public void handleWatchdog() {
		if (mMyState == EPlayerState.PLAYING
				&& mSeparateThreadMediaPlayer != null && mWriterThread != null
				&& mWriterThread.isAlive()) {
			handleBufferOverrun();

			final boolean aEnableLooping = getRemainingPlaybackTimeMs() < getPrebufferedPlaybackTimeMs();
			mSeparateThreadMediaPlayer.setLooping(aEnableLooping);

			long aNextCheckTimeout = Math.min(getRemainingPlaybackTimeMs(),
					getPrebufferedPlaybackTimeMs()) - 200;
			if (aNextCheckTimeout > 0) {
				scheduleHealthCheck(aNextCheckTimeout);
			}

			if (ConditionalLog.isEnabled()) {
				ConditionalLog.d("Rem play: " + getRemainingPlaybackTimeMs()
						+ ", adv: " + getPrebufferedPlaybackTimeMs()
						+ " next: " + aNextCheckTimeout + " Loop: "
						+ String.valueOf(aEnableLooping) + " state: "
						+ String.valueOf(mMyState));
			}

		}

		notifyBufferUpdate();

		if (mMyState == EPlayerState.UNDERRUN) {
			startIfAdvancedEnough();
			scheduleHealthCheck(3000);
		}
	}

	// Given that the smart writer is most of the time locked by I/O,
	// we need an independent thread which will turn on and off looping
	// for media player, when it comes close to buffer end

	public void scheduleHealthCheck(long iDelay) {
		mWatchdogHandler.removeMessages(0);
		mWatchdogHandler.sendMessageDelayed(
				Message.obtain(mWatchdogHandler, 0), iDelay);
	}

	public void doHealthCheck() {
		mWatchdogHandler.sendMessage(Message.obtain(mWatchdogHandler, 0));
	}

	static Handler mWatchdogHandler = new Handler(Looper.getMainLooper()) {
		public void handleMessage(Message iMsg) {
			super.handleMessage(iMsg);
			if (mFallbackPlayerToMonitor == null) {
				removeMessages(iMsg.what);
				return;
			}
			mFallbackPlayerToMonitor.handleWatchdog();
		}

	};

	Runnable mWriterSmart = new Runnable() {
		public void run() {

			if (mWatchdogHandler == null) {
				// mWatchdogHandler }
			}
			try {
				mThreadAlive = true;

				prepareAllFiles();

				notifyStartBuffering();

				while (mThreadAlive) {

					int aLength = 0;

					int aBufferDownloadGranularity = (mMyState == EPlayerState.UNDERRUN) ? 16
							: 4;

					while (aLength < mOnDiskBuffer.length
							/ aBufferDownloadGranularity) {
						aLength += mInputStream.read(mOnDiskBuffer, aLength,
								mOnDiskBuffer.length
										/ aBufferDownloadGranularity);
					}

					if (aLength == -1) {
						ConditionalLog.e("socket closed");
						mMyState = EPlayerState.IO_ERROR;
						break;
					}

					if (mCurrentRafPosition == 0
							&& mMyState == EPlayerState.UNDERRUN) {
						// File should begin with frame start
						int aFrameStart = findFrameStart(mOnDiskBuffer, aLength);
						if (aFrameStart == -1) {
							ConditionalLog.d("Could not find start frame");
							break;
						}
						mBufferFile.write(mOnDiskBuffer, aFrameStart, aLength
								- aFrameStart);
						mCurrentRafPosition += (aLength - aFrameStart);
						continue;
					}
					advanceBuffer(aLength);
					doHealthCheck();
				}
				mMyState = EPlayerState.STOPPED;
			} catch (IOException iIOEx) {
				ConditionalLog.e(" IO error on file operation ", iIOEx);
				if (mMyState == EPlayerState.PLAYING
						|| mMyState == EPlayerState.UNDERRUN
						|| mMyState == EPlayerState.PREPARING) {
					mMyState = EPlayerState.IO_ERROR;
				}
			} catch (Throwable iThrowable) {
				mMyState = EPlayerState.ERROR;
				ConditionalLog.e(" error on file operation ", iThrowable);
			} finally {
				try {
					if (mFileOut != null)
						mFileOut.close();
					if (mPrivateFile != null)
						mPrivateFile.delete();
					if (mInputStream != null)
						mInputStream.close();
					if (ConditionalLog.isEnabled()) {
						ConditionalLog.d(toString(),
								"cleanup after replay: alive flag="
										+ mThreadAlive);
					}
				} catch (IOException iException) {
					// TODO Auto-generated catch block
					ConditionalLog.e(" failed to cleanup ", iException);
				}
			}

			if (mMyState == EPlayerState.IO_ERROR) {
				RozhlasAppBaseFragmentActivity.mApplicationModel.mInetInfo
						.signalInetFailure();
				onError(mSeparateThreadMediaPlayer, 789, 987);
			}

			onCompletion(mSeparateThreadMediaPlayer);
			mThreadAlive = false;

			if (ConditionalLog.isEnabled()) {
				ConditionalLog.d(toString(), "Exited smart writer");
			}
		}

		/**
		 * 
		 */
		private void notifyStartBuffering() {
			if (ConditionalLog.isEnabled()) {
				ConditionalLog.d(this.toString(),
						"Failover playback started for url " + mStreamUri
								+ " " + mThreadAlive + mPrivateFilePath);
			}
			mOnBufferUpdateListener.onBufferingUpdate(
					mSeparateThreadMediaPlayer, 0);
		}

		/**
		 * @throws MalformedURLException
		 * @throws IOException
		 * @throws FileNotFoundException
		 */
		private void prepareAllFiles() throws MalformedURLException,
				IOException, FileNotFoundException {
			mPrivateFilePath = mContext.getFilesDir() + "/" + mBufferName;
			for (int i = 0; i < mRestOfFile.length; ++i) {
				mRestOfFile[i] = 0;
			}

			mTargetUrl = new URL(mStreamUri);

			mInputStream = mTargetUrl.openStream();
			mPrivateFile = new File(mContext.getFilesDir(), mBufferName);

			mFileOut = mContext.openFileOutput(mFallbackPlaybackFileName,
					Context.MODE_PRIVATE);

			mFileOut.write("Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn"
					.getBytes());
			mPrivateFile = new File(mPrivateFilePath);
			mFileOut = null;

			mBufferFile = new RandomAccessFile(mPrivateFile, "rw");

			mBufferFile.setLength(mRawSize);
			mBufferFile.seek(0);
		}

		/**
		 * @param iLength
		 * @throws IOException
		 */
		private void advanceBuffer(int iLength) throws IOException {
			// if (ConditionalLog.enabled()) {
			// ConditionalLog.d(this.toString(), "Advancing by " + iLength
			// + " bytes, current position is " + mCurrentRafPosition);
			// }

			if ((mCurrentRafPosition + iLength) >= mRawSize) {
				int aAvailableSpace = mRawSize - mCurrentRafPosition;
				int aFrameStart = findFrameStartReverse(mOnDiskBuffer,
						Math.min(iLength, aAvailableSpace));
				if (ConditionalLog.isEnabled()) {
					ConditionalLog.d(
							this.toString(),
							"Dummy frame for wrap, start = "
									+ String.valueOf(aFrameStart)
									+ " length = "
									+ String.valueOf(aAvailableSpace
											- aFrameStart));
				}

				if (aFrameStart != -1) {
					if (ConditionalLog.isEnabled()) {
						ConditionalLog.d(
								this.toString(),
								"Dummy frame for wrap, length = "
										+ String.valueOf(aAvailableSpace
												- aFrameStart));
					}
					if (aFrameStart > 0) {
						mBufferFile.write(mOnDiskBuffer, 0, aFrameStart);
						mCurrentRafPosition += aFrameStart;
					}
					int aLeftBytes = mRawSize - mCurrentRafPosition;
					while (aLeftBytes > 0) {
						int toWrite = Math.min(mRestOfFile.length, aLeftBytes);
						mBufferFile.write(mRestOfFile, 0, toWrite);
						aLeftBytes -= toWrite;
					}
					mBufferFile.seek(0);
					mBufferFile.write(mOnDiskBuffer, aFrameStart, iLength
							- aFrameStart);
					mCurrentRafPosition = iLength - aFrameStart;
				} else {
					if (mCurrentRafPosition + iLength > mRawSize) {
						int aLeftBytes = mRawSize - mCurrentRafPosition;
						mCurrentRafPosition = 0;
						System.out.println("lost frame end!!! :( " + iLength
								+ " bytes were not enough");
						while (aLeftBytes > 0) {
							int toWrite = Math.min(mRestOfFile.length,
									aLeftBytes);
							mBufferFile.write(mRestOfFile, 0, toWrite);
							aLeftBytes -= toWrite;
						}
						mBufferFile.seek(0);
					}
					mBufferFile.write(mOnDiskBuffer, 0, iLength);
					mCurrentRafPosition += iLength;
				}
			} else {
				mBufferFile.write(mOnDiskBuffer, 0, iLength);
				mCurrentRafPosition += iLength;
			}
		}

	};

	/**
	 * 
	 */
	private void notifyBufferUpdate() {
		if (mMyState == EPlayerState.PLAYING
				|| mMyState == EPlayerState.UNDERRUN) {
			final int aPercentage = (int) ((100.0 * getPrebufferedPlaybackTimeMs() / 1000.0) / (mPrebufferSeconds));
			mOnBufferUpdateListener.onBufferingUpdate(
					mSeparateThreadMediaPlayer, aPercentage);
			// ConditionalLog.d(this.toString(), "Buffer updated "
			// + mCurrentRafPosition + " ie " + aPercentage + "%");
		}
	}

	/**
	 * @param iBufferAdvantage
	 */
	private boolean handleBufferOverrun() {
		if (mSeparateThreadMediaPlayer != null
				&& mSeparateThreadMediaPlayer.isPlaying()
				&& isBufferUnderrunBytes()) {

			ConditionalLog.d("Buffer underrun, paused to download more");
			mMyState = EPlayerState.UNDERRUN;
			mSeparateThreadMediaPlayer.pause();
			scheduleHealthCheck(mPrebufferSeconds * 500);
			return true;
		}
		return false;
	}

	/**
	 * @param iBufferAdvantage
	 */
	void startIfAdvancedEnough() {
		if (isPreBufferEnoughBytes() && mMyState == EPlayerState.UNDERRUN) {
			restartPlayback();
		}
	}

	@Override
	public void onPrepared(MediaPlayer iPlayer) {
		if (mMyState == EPlayerState.PREPARING
				&& iPlayer == mSeparateThreadMediaPlayer) {
			ConditionalLog.d("Starting/resuming MediaPlayer for file "
					+ mPrivateFilePath);
			mMyState = EPlayerState.PLAYING;
			super.onPrepared(iPlayer);
		} else {
			ConditionalLog.e(" SEVERE PROBLEM onPrepared, in invalid state ");
		}

	}

	private void restartPlayback() {
		try {
			dispose();
			resetMediaPlayer(mPrivateFilePath);
			mSeparateThreadMediaPlayer.prepareAsync();

			mMyState = EPlayerState.PREPARING;
			// mSeparateThreadMediaPlayer.start();
			// mMyState = EPlayerState.PLAYING;

		} catch (Throwable e) {
			ConditionalLog.e(" restartPlayback ", e);
		}
	}

	// Calculates and returns buffer advantage in bytes
	protected int calculateBufferAdvantageBytes() {
		final int aCurrentBufferingPosition = mCurrentRafPosition;
		final int aCurrentPlaybackPosition = getPlaybackPositionBytes();
		final int aBufferAdvantage = aCurrentBufferingPosition > aCurrentPlaybackPosition ? aCurrentBufferingPosition
				- aCurrentPlaybackPosition
				: aCurrentBufferingPosition - aCurrentPlaybackPosition
						+ mRawSize;
		// if (ConditionalLog.enabled()) {
		// ConditionalLog.d(toString(), "Buffer advantage is: "
		// + aBufferAdvantage / mBytesPerSecond + "("
		// + aCurrentPlaybackPosition + "-"
		// + aCurrentBufferingPosition + ":" + mRawSize + ")");
		// }
		return aBufferAdvantage;
	}

	private long getRemainingPlaybackTimeMs() {
		return mDiskBufferSeconds * 1000 - getPlaybackPositionMs();
	}

	private long getPrebufferedPlaybackTimeMs() {
		return (calculateBufferAdvantageBytes() * 1000) / mBytesPerSecond;
	}

	int mLastKnownPosition = 0;

	protected int getPlaybackPositionMs() {
		if (mSeparateThreadMediaPlayer != null
				&& mSeparateThreadMediaPlayer.isPlaying()) {
			mLastKnownPosition = mSeparateThreadMediaPlayer
					.getCurrentPosition();
		}
		return mLastKnownPosition;
	}

	protected int getPlaybackPositionBytes() {
		return getPlaybackPositionMs() * mBytesPerSecond / 1000;
		// if (ConditionalLog.enabled()) {
		// ConditionalLog.d(toString(), "Playback position is: "
		// + aCurrentPositionInBytes + "( out of " + mRawSize
		// + " bytes)");
		// }
	}

}
